/*
 * $Id$
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.googlecode.jasperlib.internal;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletContext;
import javax.servlet.jsp.JspPage;
import javax.servlet.jsp.JspContext;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import javax.el.ELContext;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.googlecode.jasperlib.ClassLoaderInterface;
import com.googlecode.jasperlib.ClassLoaderInterfaceDelegate;
import com.googlecode.jasperlib.JSPRuntime;
import com.googlecode.jasperlib.TldLocationsCacheFactory;
import com.googlecode.jasperlib.MemoryClassLoader;
import com.googlecode.jasperlib.MemoryJavaFileObject;
import com.googlecode.jasperlib.internal.jasper.JasperException;
import com.googlecode.jasperlib.internal.jasper.JspC;
import com.googlecode.jasperlib.internal.jasper.compiler.TldLocationsCache;

/**
 * Uses jasper to extract a JSP from the classpath to a file and compile it. The
 * classpath used for compilation is built by finding all the jar files using
 * the current class loader (Thread), plus directories.
 */
public class JSPLoader {
    private static final Log LOG = LogFactory.getLog(JSPLoader.class);

    private static final String DEFAULT_PACKAGE = "com.googlecode.jasperlib.internal.jsp";

    private static final Pattern PACKAGE_PATTERN = Pattern.compile("package (.*?);");
    private static final Pattern CLASS_PATTERN = Pattern.compile("public final class (.*?) ");

    public Servlet load(String location, ClassLoaderInterface classLoaderInterface, List<URL> additionalJars, TldLocationsCacheFactory tldLocationsCacheFactory, MemoryClassLoader targetClassLoader, Map<String, Object> attributes) throws Exception {
        if (targetClassLoader == null)
            throw new IllegalArgumentException("MemoryClassLoader parameter cannot be null");

        
        location = StringUtils.substringBeforeLast(location, "?");

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Compiling JSP [%s]", location));
        }

        if (classLoaderInterface == null)
            classLoaderInterface = new ClassLoaderInterfaceDelegate(JSPLoader.class.getClassLoader());

        // use Jasper to compile the JSP into java code
        JspC jspC = compileJSP(location, classLoaderInterface, tldLocationsCacheFactory);
        String source = jspC.getSourceCode();

        //System.out.print(source);

        String className = extractClassName(source);

        // use Java Compiler API to compile the java code into a class
        // the tlds that were discovered are added (their jars) to the classpath
        compileJava(className, source, jspC.getTldAbsolutePaths(), classLoaderInterface, additionalJars, targetClassLoader);

        // load the class that was just built
        Class clazz = Class.forName(className, false, targetClassLoader);
        return createServlet(clazz, classLoaderInterface, attributes);
    }

    private String extractClassName(String source) {
        Matcher matcher = PACKAGE_PATTERN.matcher(source);
        matcher.find();
        String packageName = matcher.group(1);

        matcher = CLASS_PATTERN.matcher(source);
        matcher.find();
        String className = matcher.group(1);

        return packageName + "." + className;
    }

    /**
     * Creates and inits a servlet
     */
    private Servlet createServlet(Class clazz, ClassLoaderInterface classLoaderInterface, Map<String, Object> attributes) throws IllegalAccessException,
            InstantiationException, ServletException {

        JSPServletConfig config = new JSPServletConfig(new ServletContextDelegate(classLoaderInterface, attributes == null ? new HashMap<String, Object>() : attributes));

        Servlet servlet = (Servlet) clazz.newInstance();
        servlet.init(config);

        /*
         * there is no need to call JspPage.init explicitly because Jasper's JSP
         * base classe HttpJspBase.init(ServletConfig) calls: jspInit();
         * _jspInit();
         */

        return servlet;
    }

    /**
     * Compiles the given source code into java bytecode
     *
     * @param classLoaderInterface
     * @param additionalJars
     */
    private void compileJava(String className, final String source, Set<String> extraClassPath,
                             ClassLoaderInterface classLoaderInterface, List<URL> additionalJars, final MemoryClassLoader classLoader) throws IOException {
        if (LOG.isTraceEnabled())
            LOG.trace(String.format("Compiling [%s], source: [%s]", className, source));
        
        if (classLoader == null)
            throw new IllegalArgumentException("MemoryClassLoader parameter cannot be null");

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();

        // the generated bytecode is fed to the class loader
        JavaFileManager jfm = new ForwardingJavaFileManager<StandardJavaFileManager>(compiler.getStandardFileManager(diagnostics,
                null, null)) {

            @Override
            public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind,
                    FileObject sibling) throws IOException {
                MemoryJavaFileObject fileObject = new MemoryJavaFileObject(name, kind);
                classLoader.addMemoryJavaFileObject(name, fileObject);
                return fileObject;
            }
        };

        // read java source code from memory
        String fileName = className.replace('.', '/') + ".java";
        SimpleJavaFileObject sourceCodeObject = new SimpleJavaFileObject(toURI(fileName), JavaFileObject.Kind.SOURCE) {
            @Override
            public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException, IllegalStateException,
                    UnsupportedOperationException {
                return source;
            }

        };

        // build classpath
        // some entries will be added multiple times, hence the set
        List<String> optionList = new ArrayList<String>();
        Set<String> classPath = new HashSet<String>();

        // find available jars
        UrlSet urlSet = new UrlSet(classLoaderInterface);

        // find jars
        List<URL> urls = urlSet.getUrls();

        for (URL url : urls) {
            URL normalizedUrl = URLUtils.normalizeToFileProtocol(url);
            File file = FileUtils.toFile((URL) ObjectUtils.defaultIfNull(normalizedUrl, url));
            if (file.exists())
                classPath.add(file.getAbsolutePath());
        }

        // these should be in the list already, but I am feeling paranoid
        // servlet api
        classPath.add(getJarUrl(ServletContext.class));
        // jsp api
        classPath.add(getJarUrl(JspContext.class));
        //jsp el
        classPath.add(getJarUrl(ELContext.class));
        //this jar
        classPath.add(getJarUrl(JSPRuntime.class));

        // add extra classpath entries (jars where tlds were found will be here)
        for (Iterator<String> iterator = extraClassPath.iterator(); iterator.hasNext();) {
            String entry = iterator.next();
            classPath.add(entry);
        }

        if (additionalJars != null) {
            for (URL url : additionalJars) {
                URL normalizedUrl = URLUtils.normalizeToFileProtocol(url);
                File file = FileUtils.toFile((URL) ObjectUtils.defaultIfNull(normalizedUrl, url));
                if (file.exists()) {
                    classPath.add(file.getAbsolutePath());
                }
            }
        }

        String classPathString = StringUtils.join(classPath, File.pathSeparator);
        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Compiling [%s] with classpath [%s]", className, classPathString));
        }

        optionList.addAll(Arrays.asList("-classpath", classPathString));

        // compile
        JavaCompiler.CompilationTask task = compiler.getTask(null, jfm, diagnostics, optionList, null, Arrays
                .asList(sourceCodeObject));

        if (!task.call()) {
            throw new RuntimeException("Compilation failed:" + diagnostics.getDiagnostics().get(0).toString());
        }
    }

    protected String getJarUrl(Class clazz) {
        ProtectionDomain protectionDomain = clazz.getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URL loc = codeSource.getLocation();
        File file = FileUtils.toFile(loc);
        return file.getAbsolutePath();
    }

    private JspC compileJSP(String location, ClassLoaderInterface classLoaderInterface, TldLocationsCacheFactory tldLocationsCacheFactory) throws JasperException {
        JspC jspC = new JspC();
        jspC.setTldLocationsCacheFactory(tldLocationsCacheFactory);
        jspC.setClassLoaderInterface(classLoaderInterface);
        jspC.setCompile(false);
        jspC.setJspFiles(location);
        jspC.setPackage(DEFAULT_PACKAGE);
        jspC.execute();
        return jspC;
    }

    private static URI toURI(String name) {
        try {
            return new URI(name);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }
}
